﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;
using XData.Data.Components;
using XData.Data.Configuration;
using XData.Data.Element;
using XData.Data.Objects;
using XData.Data.Security;

namespace XData.Data.Services
{
    public class AccountService
    {
        protected Database Database = ConfigurationCreator.CreateElementContext().Database;
        protected static IEncryptor PasswordEncryptor = ConfigurationCreator.CreatePasswordEncryptor();

        public bool Validate(string userName, string password)
        {
            DbParameter parameter;
            string sql = GetUserSql(userName, out parameter);
            IEnumerable<XElement> result = Database.SqlQuery("User", sql, parameter);
            int count = result.Count();
            if (count == 0) return false;

            XElement user = result.First();
            return ValidatePassword(user, password);
        }

        protected bool ValidatePassword(XElement user, string password)
        {
            int format = int.Parse(user.Element("PasswordFormat").Value);
            string passwordAlgorithm = user.Element("PasswordAlgorithm").Value;
            Guid algorithm = string.IsNullOrWhiteSpace(passwordAlgorithm) ? Guid.Empty : Guid.Parse(passwordAlgorithm);
            string salt = user.Element("PasswordSalt").Value;
            return user.Element("Password").Value == EncryptPassword(password, format, algorithm, salt);
        }

        protected string EncryptPassword(string password, int format, Guid algorithm, string salt)
        {
            if (format == (int)PasswordFormat.Clear) return password;
            if (format == (int)PasswordFormat.Hashed)
            {
                IEncryptor encryptor = ConfigurationCreator.Hashers[algorithm];
                return encryptor.Encrypt(password, salt);
            }
            if (format == (int)PasswordFormat.Encrypted)
            {
                IEncryptor encryptor = ConfigurationCreator.Cryptors[algorithm];
                return encryptor.Encrypt(password, salt);
            }

            throw new NotSupportedException(format.ToString());
        }

        public XElement GetUser(string userName)
        {
            DbParameter parameter;
            string sql = GetUserSql(userName, out parameter);
            IEnumerable<XElement> result = Database.SqlQuery("User", sql, parameter);
            return result.First();
        }

        protected string GetUserSql(string userName, out DbParameter parameter)
        {
            if (Database is SqlDatabase) // XData.Data.Objects.SqlDatabase
            {
                string sql = string.Format("SELECT * FROM Users WHERE LoweredUserName = @UserName", userName.ToLower());
                parameter = Database.CreateDbParameter("@UserName", userName);
                return sql;
            }

            string typeName = Database.GetType().FullName;
            if (typeName == "XData.Data.Objects.OracleDatabase")
            {
                string sql = string.Format("SELECT * FROM Users WHERE LoweredUserName = :UserName", userName.ToLower());
                parameter = Database.CreateDbParameter(":UserName", userName);
                return sql;
            }

            throw new NotSupportedException(typeName);
        }

        public void SetPassword(string userName, string newPassword)
        {
            XElement user = GetUser(userName);
            int id = int.Parse(user.Element("Id").Value);
            SetPassword(id, newPassword, Database);
        }

        internal protected static string Encrypt(string password, out int format, out Guid algorithm, out string salt)
        {
            string encrypted;
            if (PasswordEncryptor == null)
            {
                format = (int)PasswordFormat.Clear;
                algorithm = Guid.Empty;
                salt = string.Empty;
                encrypted = password;
            }
            else if (PasswordEncryptor is Hasher)
            {
                format = (int)PasswordFormat.Hashed;
                algorithm = PasswordEncryptor.GetType().GUID;
                salt = GenerateSalt();
                encrypted = PasswordEncryptor.Encrypt(password, salt);
            }
            else if (PasswordEncryptor is Cryptor)
            {
                format = (int)PasswordFormat.Encrypted;
                algorithm = PasswordEncryptor.GetType().GUID;
                salt = GenerateSalt();
                encrypted = PasswordEncryptor.Encrypt(password, salt);
            }
            else
            {
                throw new NotSupportedException(); // never
            }

            return encrypted;
        }

        internal protected static void SetPassword(int id, string newPassword, Database database)
        {
            int format;
            Guid algorithm;
            string salt;
            string encrypted = Encrypt(newPassword, out format, out algorithm, out salt);

            DbParameter[] parameter;
            string sql = GetSetPasswordSql(database, id, encrypted, format, algorithm, salt, out parameter);
            int i = database.ExecuteSqlCommand(sql, parameter);
        }

        protected static string GetSetPasswordSql(Database database, int id, string password, int format, Guid algorithm, string salt, out DbParameter[] parameter)
        {
            if (database is SqlDatabase)
            {
                string sql = @"
 UPDATE Users
 SET Password = @Password,
 PasswordFormat = @Format,
 PasswordAlgorithm = @Algorithm,
 PasswordSalt = @Salt
 WHERE Id = {0}";
                sql = string.Format(sql, id);
                parameter = new DbParameter[4];
                parameter[0] = database.CreateDbParameter("@Password", password);
                parameter[1] = database.CreateDbParameter("@Format", format);
                parameter[2] = database.CreateDbParameter("@Algorithm", algorithm);
                parameter[3] = database.CreateDbParameter("@Salt", salt);
                return sql;
            }

            string typeName = database.GetType().FullName;
            if (typeName == "XData.Data.Objects.OracleDatabase")
            {
                string sql = @"
 UPDATE Users
 SET Password = :Password,
 PasswordFormat = :Format,
 PasswordAlgorithm = :Algorithm,
 PasswordSalt = :Salt
 WHERE Id = {0}";
                sql = string.Format(sql, id);
                parameter = new DbParameter[4];
                parameter[0] = database.CreateDbParameter(":Password", password);
                parameter[1] = database.CreateDbParameter(":Format", format);
                parameter[2] = database.CreateDbParameter(":Algorithm", algorithm);
                parameter[3] = database.CreateDbParameter(":Salt", salt);
                return sql;
            }

            throw new NotSupportedException(typeName);
        }

        protected static string GenerateSalt()
        {
            byte[] array = new byte[16];
            new RNGCryptoServiceProvider().GetBytes(array);
            return Convert.ToBase64String(array);
        }

        public string GenerateApiVerificationToken()
        {
            byte[] array = new byte[64];
            new RNGCryptoServiceProvider().GetBytes(array);
            return Convert.ToBase64String(array);
        }

        public string EncryptApiVerificationToken(string apiVerificationToken)
        {
            return new ApiVerificationTokenCryptor().Encrypt(apiVerificationToken);
        }

        protected class ApiVerificationTokenCryptor : RijndaelCryptor
        {
            protected const string KEY = "AhfReb3kNp86H1vdFnqSZnliaPQi6GvIdDjukiAig5s=";
            protected const string IV = "mqxvh+NGXcUdR31XtIFUlQ==";

            protected override string GetKey()
            {
                return KEY;
            }

            protected override string GetIV()
            {
                return IV;
            }

        }

    }
}